index.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. import { PermissionAction } from '@supabase/shared-types/out/constants'
  2. import { IS_PLATFORM, useFeatureFlags, useFlag, useParams } from 'common'
  3. import dayjs, { Dayjs } from 'dayjs'
  4. import maxBy from 'lodash/maxBy'
  5. import meanBy from 'lodash/meanBy'
  6. import sumBy from 'lodash/sumBy'
  7. import { useRouter } from 'next/router'
  8. import { useMemo, useState } from 'react'
  9. import { Alert, AlertDescription, AlertTitle, Button, LogoLoader, WarningIcon } from 'ui'
  10. import { PageContainer } from 'ui-patterns/PageContainer'
  11. import { PageSection, PageSectionContent } from 'ui-patterns/PageSection'
  12. import { EdgeFunctionOverview } from '@/components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionOverview'
  13. import { EdgeFunctionRecentInvocations } from '@/components/interfaces/Functions/EdgeFunctionRecentInvocations'
  14. import ReportWidget from '@/components/interfaces/Reports/ReportWidget'
  15. import DefaultLayout from '@/components/layouts/DefaultLayout'
  16. import EdgeFunctionDetailsLayout from '@/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout'
  17. import AreaChart from '@/components/ui/Charts/AreaChart'
  18. import StackedBarChart from '@/components/ui/Charts/StackedBarChart'
  19. import NoPermission from '@/components/ui/NoPermission'
  20. import {
  21. FunctionsCombinedStatsVariables,
  22. useFunctionsCombinedStatsQuery,
  23. } from '@/data/analytics/functions-combined-stats-query'
  24. import { useEdgeFunctionQuery } from '@/data/edge-functions/edge-function-query'
  25. import { useFillTimeseriesSorted } from '@/hooks/analytics/useFillTimeseriesSorted'
  26. import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
  27. import type { ChartIntervals, NextPageWithLayout } from '@/types'
  28. const CHART_INTERVALS: ChartIntervals[] = [
  29. {
  30. key: '15min',
  31. label: '15 min',
  32. startValue: 15,
  33. startUnit: 'minute',
  34. format: 'MMM D, h:mm:ssa',
  35. },
  36. {
  37. key: '1hr',
  38. label: '1 hour',
  39. startValue: 1,
  40. startUnit: 'hour',
  41. format: 'MMM D, h:mma',
  42. },
  43. {
  44. key: '3hr',
  45. label: '3 hours',
  46. startValue: 3,
  47. startUnit: 'hour',
  48. format: 'MMM D, h:mma',
  49. },
  50. {
  51. key: '1day',
  52. label: '1 day',
  53. startValue: 1,
  54. startUnit: 'hour',
  55. format: 'MMM D, h:mma',
  56. },
  57. ]
  58. const LegacyEdgeFunctionOverview = () => {
  59. const router = useRouter()
  60. const { ref: projectRef, functionSlug } = useParams()
  61. const [interval, setInterval] = useState<string>('15min')
  62. const selectedInterval = CHART_INTERVALS.find((i) => i.key === interval) || CHART_INTERVALS[1]
  63. const { data: selectedFunction } = useEdgeFunctionQuery({
  64. projectRef,
  65. slug: functionSlug,
  66. })
  67. const id = selectedFunction?.id
  68. const combinedStatsResults = useFunctionsCombinedStatsQuery({
  69. projectRef,
  70. functionId: id,
  71. interval: selectedInterval.key as FunctionsCombinedStatsVariables['interval'],
  72. })
  73. const combinedStatsData = useMemo(() => {
  74. const result = combinedStatsResults.data?.result as
  75. | Record<string, string | number>[]
  76. | undefined
  77. return result || []
  78. }, [combinedStatsResults.data])
  79. const [startDate, endDate]: [Dayjs, Dayjs] = useMemo(() => {
  80. const start = dayjs()
  81. .subtract(selectedInterval.startValue, selectedInterval.startUnit as dayjs.ManipulateType)
  82. .startOf(selectedInterval.startUnit as dayjs.ManipulateType)
  83. const end = dayjs().startOf(selectedInterval.startUnit as dayjs.ManipulateType)
  84. return [start, end]
  85. }, [selectedInterval])
  86. const {
  87. data: combinedStatsChartData,
  88. error: combinedStatsError,
  89. isError: isErrorCombinedStats,
  90. } = useFillTimeseriesSorted({
  91. data: combinedStatsData,
  92. timestampKey: 'timestamp',
  93. valueKey: [
  94. 'requests_count',
  95. 'log_count',
  96. 'log_info_count',
  97. 'log_warn_count',
  98. 'log_error_count',
  99. 'success_count',
  100. 'redirect_count',
  101. 'client_err_count',
  102. 'server_err_count',
  103. 'avg_cpu_time_used',
  104. 'avg_memory_used',
  105. 'avg_execution_time',
  106. 'max_execution_time',
  107. 'avg_heap_memory_used',
  108. 'avg_external_memory_used',
  109. 'max_cpu_time_used',
  110. ],
  111. defaultValue: 0,
  112. startDate: startDate.toISOString(),
  113. endDate: endDate.toISOString(),
  114. })
  115. const { isLoading: permissionsLoading, can: canReadFunction } = useAsyncCheckPermissions(
  116. PermissionAction.FUNCTIONS_READ,
  117. functionSlug as string
  118. )
  119. if (!canReadFunction && !permissionsLoading) {
  120. return <NoPermission isFullPage resourceText="access this edge function" />
  121. }
  122. return (
  123. <PageContainer size="full">
  124. <PageSection>
  125. <PageSectionContent>
  126. {IS_PLATFORM && id && (
  127. <div className="mb-8">
  128. <EdgeFunctionRecentInvocations
  129. functionId={id}
  130. functionSlug={functionSlug as string}
  131. />
  132. </div>
  133. )}
  134. <div className="flex flex-row items-center gap-2 mb-4">
  135. <div className="flex items-center">
  136. {CHART_INTERVALS.map((item, i) => {
  137. const classes = []
  138. if (i === 0) {
  139. classes.push('rounded-tr-none rounded-br-none')
  140. } else if (i === CHART_INTERVALS.length - 1) {
  141. classes.push('rounded-tl-none rounded-bl-none')
  142. } else {
  143. classes.push('rounded-none')
  144. }
  145. return (
  146. <Button
  147. key={`function-filter-${i}`}
  148. type={interval === item.key ? 'secondary' : 'default'}
  149. onClick={() => setInterval(item.key)}
  150. className={classes.join(' ')}
  151. >
  152. {item.label}
  153. </Button>
  154. )
  155. })}
  156. </div>
  157. <span className="text-xs text-foreground-light">
  158. Statistics for past {selectedInterval.label}
  159. </span>
  160. </div>
  161. <div>
  162. <div className="grid grid-cols-1 md:grid-cols-2 md:gap-4 lg:grid-cols-2 lg:gap-8">
  163. <ReportWidget
  164. title="Execution time"
  165. tooltip="Average execution time of function invocations"
  166. data={combinedStatsChartData}
  167. isLoading={combinedStatsResults.isLoading}
  168. renderer={(props) => {
  169. return isErrorCombinedStats ? (
  170. <Alert variant="warning">
  171. <WarningIcon />
  172. <AlertTitle>Failed to reterieve execution time</AlertTitle>
  173. <AlertDescription>
  174. {combinedStatsError?.message ?? 'Unknown error'}
  175. </AlertDescription>
  176. </Alert>
  177. ) : (
  178. <div className="space-y-8">
  179. <AreaChart
  180. title="Average execution time"
  181. className="w-full"
  182. xAxisKey="timestamp"
  183. customDateFormat={selectedInterval.format}
  184. yAxisKey="avg_execution_time"
  185. data={props.data}
  186. format="ms"
  187. highlightedValue={meanBy(props.data, 'avg_execution_time')}
  188. />
  189. <AreaChart
  190. title="Max execution time"
  191. className="w-full"
  192. xAxisKey="timestamp"
  193. customDateFormat={selectedInterval.format}
  194. yAxisKey="max_execution_time"
  195. data={props.data}
  196. format="ms"
  197. highlightedValue={
  198. maxBy(props.data, 'max_execution_time')?.max_execution_time
  199. }
  200. />
  201. </div>
  202. )
  203. }}
  204. />
  205. <ReportWidget
  206. title="Invocations"
  207. tooltip="Requests made to a function are considered invocations, and each invocation may have worker logs."
  208. data={combinedStatsChartData}
  209. isLoading={combinedStatsResults.isLoading}
  210. renderer={(props) => {
  211. if (isErrorCombinedStats) {
  212. return (
  213. <Alert variant="warning">
  214. <WarningIcon />
  215. <AlertTitle>Failed to reterieve invocations</AlertTitle>
  216. <AlertDescription>
  217. {combinedStatsError?.message ?? 'Unknown error'}
  218. </AlertDescription>
  219. </Alert>
  220. )
  221. } else {
  222. const requestData = props.data
  223. .map((d: any) => [
  224. {
  225. status: '2xx',
  226. count: d.success_count,
  227. timestamp: d.timestamp,
  228. },
  229. {
  230. status: '3xx',
  231. count: d.redirect_count,
  232. timestamp: d.timestamp,
  233. },
  234. {
  235. status: '4xx',
  236. count: d.client_err_count,
  237. timestamp: d.timestamp,
  238. },
  239. {
  240. status: '5xx',
  241. count: d.server_err_count,
  242. timestamp: d.timestamp,
  243. },
  244. ])
  245. .flat()
  246. const logsData = props.data
  247. .map((d: any) => [
  248. {
  249. status: 'error',
  250. count: d.log_error_count,
  251. timestamp: d.timestamp,
  252. },
  253. {
  254. status: 'info',
  255. count: d.log_info_count,
  256. timestamp: d.timestamp,
  257. },
  258. {
  259. status: 'warn',
  260. count: d.log_warn_count,
  261. timestamp: d.timestamp,
  262. },
  263. ])
  264. .flat()
  265. return (
  266. <div className="space-y-8">
  267. <StackedBarChart
  268. title="Invocation Requests"
  269. className="w-full"
  270. xAxisKey="timestamp"
  271. yAxisKey="count"
  272. stackKey="status"
  273. data={requestData}
  274. highlightedValue={sumBy(requestData, 'count')}
  275. customDateFormat={selectedInterval.format}
  276. stackColors={['brand', 'slate', 'yellow', 'red']}
  277. onBarClick={() => {
  278. router.push(
  279. `/project/${projectRef}/functions/${functionSlug}/invocations?its=${startDate.toISOString()}`
  280. )
  281. }}
  282. />
  283. <StackedBarChart
  284. title="Worker Logs"
  285. className="w-full"
  286. xAxisKey="timestamp"
  287. yAxisKey="count"
  288. stackKey="status"
  289. data={logsData}
  290. highlightedValue={sumBy(logsData, 'count')}
  291. customDateFormat={selectedInterval.format}
  292. stackColors={['red', 'brand', 'yellow']}
  293. onBarClick={() => {
  294. router.push(
  295. `/project/${projectRef}/functions/${functionSlug}/logs?its=${startDate.toISOString()}`
  296. )
  297. }}
  298. />
  299. </div>
  300. )
  301. }
  302. }}
  303. />
  304. <ReportWidget
  305. title="CPU time"
  306. tooltip="Average CPU time usage for the function"
  307. data={combinedStatsChartData}
  308. isLoading={combinedStatsResults.isLoading}
  309. renderer={(props) => {
  310. return isErrorCombinedStats ? (
  311. <Alert variant="warning">
  312. <WarningIcon />
  313. <AlertTitle>Failed to retrieve CPU time</AlertTitle>
  314. <AlertDescription>
  315. {combinedStatsError?.message ?? 'Unknown error'}
  316. </AlertDescription>
  317. </Alert>
  318. ) : (
  319. <div className="space-y-8">
  320. <AreaChart
  321. title="Average CPU Time"
  322. className="w-full"
  323. xAxisKey="timestamp"
  324. customDateFormat={selectedInterval.format}
  325. yAxisKey="avg_cpu_time_used"
  326. data={props.data}
  327. format="ms"
  328. highlightedValue={meanBy(props.data, 'avg_cpu_time_used')}
  329. />
  330. <AreaChart
  331. title="Max CPU Time"
  332. className="w-full"
  333. xAxisKey="timestamp"
  334. customDateFormat={selectedInterval.format}
  335. yAxisKey="max_cpu_time_used"
  336. data={props.data}
  337. format="ms"
  338. highlightedValue={maxBy(props.data, 'max_cpu_time_used')?.max_cpu_time_used}
  339. />
  340. </div>
  341. )
  342. }}
  343. />
  344. <ReportWidget
  345. title="Memory"
  346. tooltip="Average memory usage for the function"
  347. data={combinedStatsChartData}
  348. isLoading={combinedStatsResults.isLoading}
  349. renderer={(props) => {
  350. if (isErrorCombinedStats) {
  351. return (
  352. <Alert variant="warning">
  353. <WarningIcon />
  354. <AlertTitle>Failed to retrieve memory usage</AlertTitle>
  355. <AlertDescription>
  356. {combinedStatsError?.message ?? 'Unknown error'}
  357. </AlertDescription>
  358. </Alert>
  359. )
  360. }
  361. const memoryData = props.data
  362. .map((d: any) => [
  363. {
  364. type: 'heap',
  365. count: d.avg_heap_memory_used,
  366. timestamp: d.timestamp,
  367. },
  368. {
  369. type: 'external',
  370. count: d.avg_external_memory_used,
  371. timestamp: d.timestamp,
  372. },
  373. ])
  374. .flat()
  375. return (
  376. <div className="space-y-8">
  377. <AreaChart
  378. title="Average Memory Usage"
  379. className="w-full"
  380. xAxisKey="timestamp"
  381. customDateFormat={selectedInterval.format}
  382. yAxisKey="avg_memory_used"
  383. data={props.data}
  384. format="MB"
  385. highlightedValue={meanBy(props.data, 'avg_memory_used')}
  386. />
  387. <StackedBarChart
  388. title="Average Memory Usage by Type"
  389. className="w-full"
  390. xAxisKey="timestamp"
  391. yAxisKey="count"
  392. stackKey="type"
  393. format="MB"
  394. data={memoryData}
  395. highlightedValue={sumBy(memoryData, 'count')}
  396. customDateFormat={selectedInterval.format}
  397. stackColors={['blue', 'brand']}
  398. />
  399. </div>
  400. )
  401. }}
  402. />
  403. </div>
  404. </div>
  405. </PageSectionContent>
  406. </PageSection>
  407. </PageContainer>
  408. )
  409. }
  410. const PageLayout: NextPageWithLayout = () => {
  411. const { hasLoaded: flagsLoaded } = useFeatureFlags()
  412. const showNewOverview = useFlag('edgeFunctionsOverview') === true
  413. if (IS_PLATFORM && !flagsLoaded) {
  414. return <LogoLoader />
  415. }
  416. if (showNewOverview) {
  417. return <EdgeFunctionOverview />
  418. }
  419. return <LegacyEdgeFunctionOverview />
  420. }
  421. PageLayout.getLayout = (page) => (
  422. <DefaultLayout>
  423. <EdgeFunctionDetailsLayout title="Overview">{page}</EdgeFunctionDetailsLayout>
  424. </DefaultLayout>
  425. )
  426. export default PageLayout